package scuts.smacro;

#if (macro || display)
typedef GType = Type;
typedef SType = scuts.macro.Type;
import haxe.macro.Compiler;
import haxe.macro.Type.ClassType;
import haxe.macro.Context;
import neko.FileSystem;
import neko.io.File;
import scuts.macro.Convert;
import scuts.macro.Parse;
import scuts.macro.Print;

import haxe.macro.Expr;
import haxe.macro.Type;

import scuts.CoreTypes;
using scuts.Core;
#end

class MacroContextImp 
{
  public static function exprToString(expr:haxe.macro.Expr):String
  {
    return scuts.macro.Print.exprStr(expr);
  }  
}

class Builder 
{
  #if macro
  static var registry:Hash<{fields:Array<Field>, cl:ClassType}> = new Hash();
  #end
  
  @:macro public static function build(macroExprs:Array<Expr>):Type 
  {
    return doBuild(macroExprs);
    
  }
  
  @:macro public static function register():Array<Field>
  {
    var localClass = Context.getLocalClass().get();
    
    var fields = Context.getBuildFields();
    var typeName = SType.getFullQualifiedTypeName(localClass);
    trace(typeName);
    registry.set(typeName, {fields:fields, cl:localClass});
    // remove all fields from beeing generated
    return [];
  }
  
  @:macro public static function fromClass(cl:String):Type 
  {
    Compiler.exclude("haxe.macro", true);
    
    //trace(cl);
    Compiler.addMetadata("@:macro", cl);
    Compiler.addMetadata("@:build(scuts.smacro.Builder.register())", cl);
    
    Context.getType(cl);
    //trace(registry);
    if (registry.exists(cl)) {
      var entry = registry.get(cl);
      var exprs = [];
      for (f in entry.fields) {
        switch (f.kind) {
          case FieldType.FFun(fn):
            exprs.push( { expr: EFunction(f.name, fn), pos: f.pos } );
          default:
        }
      }
      return doBuild(exprs);
    } else {
      throw "assert";
    }
    
  }
  
  #if (macro || display)
  
  public static function doBuild (macroExprs:Array<Expr>):Type {
    var className = "Make_Macro_" + Context.signature(macroExprs);
    
    var macros:Array<String> = [];
    var vars = null;
    
    for (m in macroExprs) {
      var name:String = null;
      var expr:Expr = null;
      var needsContext = false;
      var contextName:String = "";
      var macroType = null;
      switch (m.expr) {
        case EFunction(n, f):
          
          name = n;
          vars = [];
          for (a in f.args) {
            
            if (a.type != null && Print.complexTypeStr(a.type) == "scuts.smacro.MacroContext") {
              if (needsContext) {
                throw new haxe.macro.Expr.Error("Please define the MacroContext only once", m.pos);
              }
              needsContext = true;
              contextName = a.name;
              continue;
            }
            
            
            var allowedTypeDecl = a.type == null ? { valid:true, hasExprs:false} : isAllowedTypeDecl(a.type);
            
            if (!allowedTypeDecl.valid) throw new haxe.macro.Expr.Error("Not allowed Type Declaration: " + Print.complexTypeStr(a.type), m.pos);
            
            var typeStr = if (a.type == null) null else Print.complexTypeStr(a.type);
            
            var t = if (a.type == null) {
              "haxe.macro.Expr";
            } else if (StringTools.startsWith(typeStr, "ExprRequire")) {
              "haxe.macro.Expr." + typeStr;
            } else if (allowedTypeDecl.hasExprs || typeStr == "EReg") {
              "haxe.macro.Expr";
            } else {
              // Constant declaration
              // We need to take care of 
              Print.complexTypeStr(a.type);
            }
            

            var c = if (a.type != null && (allowedTypeDecl.hasExprs || typeStr == "EReg")) {
              "\t\tvar " + a.name + " = " + createTypeConversionString(a.type, a.name) + ";";
            } else {
              "";
            }
            
            vars.push( {  name:a.name, type:t, convert: c } );
            
            
          }
          
          
          var t = if (f.ret != null) {
            Convert.complexTypeToType(f.ret);
          } else {
            // TODO create a function that replaces all Types with the full qualified types 
            // inside of an Expression based on an imports parameter to bypass typeof problems
            var funcCopy = {
           
              args : f.args.map(F.n(fa = { name: fa.name, opt:fa.opt, type:fa.type, value:fa.value})),
              ret : f.ret,
              expr : f.expr,
              params : f.params,
          
            }
            
            
            var callExpr = { expr:ECall(
              {expr:EParenthesis(
                {expr:EFunction(null, funcCopy), pos:m.pos }
              ), pos:m.pos }, 
              f.args.map(F.n( { expr:EConst(CIdent("null")), pos:m.pos } ))), 
              pos:m.pos };
            try {
               Context.typeof(callExpr);
            } catch (e:haxe.macro.Expr.Error) {
              
              throw new haxe.macro.Expr.Error(
                "Cannot determine Type of function " + n + " please add a return type to your function", m.pos);
            }
          }
          

          //trace(t);
          //trace(Print.exprStr(m));
          
          switch (t) {
            case TInst(c, _):
              if (c.get().name == "String") {
                
                expr = f.expr;
                macroType = "String";
              } else {
                
                var tstr = Print.typeStr(t);
                throw new haxe.macro.Expr.Error(
                  "Your Macro Function returns either a not supported Type only Expr and String are allowed or the type cannot be determined. Currently the type " + tstr + " is detected. Please add a return type to your macro.", m.pos);
              }
            case TType(c, _):
              if (c.get().name == "Expr" && 
                  (c.get().pack.length == 0 || (c.get().pack.length == 2) && c.get().pack.join(".") == "haxe.macro")) {
                expr = f.expr;
                macroType = "Expr";
              } else {
                
                var tstr = Print.typeStr(t);
                throw new haxe.macro.Expr.Error(
                  "Your Macro Function returns either a not supported Type only Expr and String are allowed or the type cannot be determined. Currently the type " + tstr + " is detected. Please add a return type to your macro.", m.pos);
              }
              
            default: 
              
              var tstr = Print.typeStr(t);
              throw new haxe.macro.Expr.Error(
                "Your Macro Function returns either a not supported Type only Expr and String are allowed or the type cannot be determined. Currently the type " + tstr + " is detected. Please add a return type to your macro.", m.pos);
          }
          
        default: throw "assert";
      }
      
      var signature = {
        var s = "";
        var first = true;
        for (v in vars) {
          if (!first) {
            s += ",";
          } else {
            first = false;
          }
          
          s += v.name + ":" + v.type;
        }
        s;
      }
      //var signature = vars.join(':haxe.macro.Expr, ') + ((vars.length > 0) ? ":haxe.macro.Expr" : "");
      var context = "{";
      for (v in vars) {
        context += v.name + ":" + v.name + ",";
      }
      context += "}";
      var converts = "";
      if (needsContext) {
        converts += "\t\tvar " + contextName + ":scuts.smacro.MacroContext = scuts.smacro.Builder.MacroContextImp;\n";
      }
      for (v in vars) {
        converts += v.convert;
      }
      
      var fieldExpr = '@:macro public static function ' + name + '(' + signature + '):haxe.macro.Expr {\n
    var e = try {
' + converts + '
      ' + (if (macroType == "String") 'scuts.macro.Parse.parse(' else '') + '
        (function ()\n' + Print.exprStr(expr,5) + ')()' +  (if (macroType == "String") (', ' + context + ')') else '') + ';
    } catch (error:Dynamic) {
      
      haxe.macro.Context.error(error.message, error.pos);
    }
    return e;
  }
      ';
      macros.push(fieldExpr);
    }
    
    var imports = "";
    imports += "#if macro\n";
    imports += "import scuts.macro.Query;\n";
    imports += "using scuts.macro.Query;\n";
    imports += "import scuts.CoreTypes;\n";
    imports += "using scuts.Core;\n";
    imports += "import haxe.macro.Expr;\n";
    imports += "import haxe.macro.Context;\n";
    imports += "import scuts.macro.Select;\n";
    
    
    imports += "#end\n";
    
    var classExpr = imports + "private class Helper { \n\t@:macro public static function run2():haxe.macro.Type { \n\t\t " + className + "; \n\t\treturn haxe.macro.Context.getType('String');\n\t}\n\n}\nprivate typedef HelperType = haxe.macro.MacroType<(_" + className + ".Helper.run2())>;\n\nclass " + className + " {\n\t" + macros.join("\n\n\t") + "\n}";
    var output = File.write(className + ".hx");
    output.writeString(classExpr);
    output.close();
    var t = Context.getType(className);
    //FileSystem.deleteFile(className + ".hx");
    
    
    var t = Context.getType(className);
    
    
    
    return t;
  }
  
  public static function isAllowedTypeDecl (t:ComplexType):{ valid:Bool, hasExprs:Bool}
  {
    
    var hasExprs = false;
    function f1 (t:ComplexType):Bool {
      return switch (t) {
        case TPath(p):
          
          switch (p.name) {
            case "Int": p.pack.length == 0;
            case "Float": p.pack.length == 0;
            case "String": p.pack.length == 0;
            case "EReg": p.pack.length == 0;
            case "Expr": 
              hasExprs = true;
              if (p.pack.length > 0) {
                if (p.pack.join(".") != "haxe.macro") false else true;
              } else 
                true;
            case "ExprRequire":
              hasExprs = true;
              true;
            case "Array":
              p.pack.length == 0 && 
                if (p.params.length != 1) 
                  false
                else
                  switch (p.params[0]) {
                    case TPType(t): 
                      f1(t);
                    default: 
                      false;
                  }
            default: false;  
          }
        case TAnonymous(fields):
          var isConstant = true;
          for (f in fields) {
            switch (f.kind) {
              case FieldType.FVar(t, _):
                if (!f1(t)) {
                  isConstant = false;
                  break;
                }
                
              default: 
                isConstant = false;
                break;
            }
          }
          isConstant;
        default: 
          false;
      }
    }
    var valid = f1(t);
    return { valid:valid, hasExprs:hasExprs}
  }
  
  public static function createTypeConversionString (t:ComplexType, varName:String):String
  {
    
    return switch (t) {
      case TPath(p):
        
        switch (p.name) {
          case "Int": 
            if (p.pack.length > 0) throw "assert";
            'switch(' + varName + '.expr) {
              case EConst(c): 
                switch (c) {
                  case CInt(i): Std.parseInt(i);
                  default: 
                    throw new haxe.macro.Expr.Error(
                    "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                    ' + varName + '.pos
                  );
                }
              default: 
                throw new haxe.macro.Expr.Error(
                  "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                  ' + varName + '.pos
                );
            }';
          case "Float":
            if (p.pack.length > 0) throw "assert";
            'switch(' + varName + '.expr) {
              case EConst(c): 
                switch (c) {
                  case CFloat(f): Std.parseFloat(f);
                  default:
                    throw new haxe.macro.Expr.Error(
                      "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                      ' + varName + '.pos
                    );
                }
              default: 
                throw new haxe.macro.Expr.Error(
                  "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                  ' + varName + '.pos
                );
            }';
          case "String":
            if (p.pack.length > 0) throw "assert";
            'switch(' + varName + '.expr) {
              case EConst(c): 
                switch (c) {
                  case CString(s): s;
                  default: 
                    throw new haxe.macro.Expr.Error(
                      "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                      ' + varName + '.pos
                    );
                }
              default:
                throw new haxe.macro.Expr.Error(
                  "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                  ' + varName + '.pos
                );
            }';
          case "EReg":
            if (p.pack.length > 0) throw "assert";
            'switch(' + varName + '.expr) {
              case EConst(c): 
                switch (c) {
                  case CRegexp(r, opt): new EReg(r, opt);
                  default: 
                    throw new haxe.macro.Expr.Error(
                      "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                      ' + varName + '.pos
                    );
                }
              default:
                throw new haxe.macro.Expr.Error(
                  "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                  ' + varName + '.pos
                );
            }';
          case "Expr": 
            
            if (p.pack.length > 0) {
              var pack = p.pack.join(".");
              if (pack != "haxe.macro") throw "assert";
            }
            varName;
          case "ExprRequire":
             if (p.pack.length > 0) {
              var pack = p.pack.join(".");
              if (pack != "haxe.macro") throw "assert";
            }
            if (p.params.length != 1) 
              throw "assert";
            var elemType = switch (p.params[0]) {
              case TPType(t): 
                t;
              default: 
                throw "assert";
            }
            var elemTypeStr = Print.complexTypeStr(elemType);
            var testString = '{ var __e : ' + elemTypeStr + '= $expr; __e; }';
            '{
              var __r = scuts.macro.Parse.parse("' + testString + '", { expr : ' + varName + ' } );
              try {
                haxe.macro.Context.typeof(__r);
              } catch (e:haxe.macro.Expr.Error) {
                haxe.macro.Context.error(e.message, ' + varName + '.pos);
              }
              ' + varName + ';
            }';
          case "Array":
            if (p.params.length != 1) 
              throw "assert";
            else {
              var elemType = switch (p.params[0]) {
                case TPType(t): 
                  t;
                default: 
                  throw "assert";
              }
              'switch (' + varName + '.expr) {
                case EArrayDecl(__exprs):
                  var __values = [];
                  try {
                    for (__e in __exprs) {
                      __values.push(' + createTypeConversionString(elemType, "__e") + ');
                    }
                  } catch (e: haxe.macro.Expr.Error ) {
                    
                    throw new haxe.macro.Expr.Error(
                      "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                      ' + varName + '.pos);
                  }
                  __values;
                default: 
                  throw new haxe.macro.Expr.Error(
                    "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                    ' + varName + '.pos
                  );
              }';
            }
              
          default:
            throw "assert";
        }
      case TAnonymous(fields):
        var namesAndTypes = [];
        for (f in fields) {
          //trace(f);
          switch (f.kind) {
            case FieldType.FVar(t, _):
              namesAndTypes.push( { n:f.name, t: t } );
            default: 
              throw "assert";
          }
        }
        var resDecl = {
          var s = "{\n";
          var i = "{";
          for (e in namesAndTypes) {
            var t = Print.complexTypeStr(e.t);
            if (t == "Expr") t = "haxe.macro.Expr";
            if (StringTools.startsWith(t, "ExprRequire")) t = "haxe.macro.Expr";
            s += "\t" + e.n + " : " + t + ",\n";
            i += "\t" + e.n + " : null,\n";
          }
          i += "}";
          s += "}";
          s + " = " + i;
        }
        var numFields = namesAndTypes.length;
        var fieldsStr = {
          var s = "";
          for (i in 0...namesAndTypes.length) {
            var e = namesAndTypes[i];
            if (i > 0) s += "else ";
            s += "if (f.field == \"" + e.n + "\") { try { res." + e.n + " = " + createTypeConversionString(e.t, "f.expr") + ";} catch (e:haxe.macro.Expr.Error) { throw new haxe.macro.Expr.Error('Field " + e.n + " should be of Type " + scuts.macro.Print.complexTypeStr(e.t) + "', f.expr.pos);}}\n";
          }
          s;
        }
        'switch (' + varName + '.expr) {
          case EObjectDecl(fields):
            var res:' + resDecl + ';
            if (fields.length != ' + numFields + ') 
              throw new haxe.macro.Expr.Error(
                "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                ' + varName + '.pos
              );
            for (f in fields) {'
              + fieldsStr + '
              else throw new haxe.macro.Expr.Error(
                "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
                ' + varName + '.pos
              );
            }
            res;
          default: 
            throw new haxe.macro.Expr.Error(
              "Argument ' + varName + ' should be of Type ' + scuts.macro.Print.complexTypeStr(t) + '",
              ' + varName + '.pos
            );
        }';
      default: 
        throw 'assert';
    }
  }
  #end
  
}